using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace UnitTesting.Fixturing
{
	class Injector<T> : IInjector<T>
	{
		readonly T _target;
		readonly IValueProvider _valueProvider;
		readonly MethodVisibilityChecker _methodVisibilityChecker;

		internal Injector(T target, IValueProvider valueProvider)
		{
			_target = (T) Ensure.IsNotNull((object) target, "target");
			_valueProvider = Ensure.IsNotNull(() => valueProvider);
			_methodVisibilityChecker = new MethodVisibilityChecker(GetVisibilities());
		}

		Visibilities GetVisibilities()
		{
			bool isInternalVisibleToDynamicProxy = DynamicProxyReflector.IsInternalVisibleType(_target.GetType());
			var visibility = isInternalVisibleToDynamicProxy
			                 	? Visibilities.Public | Visibilities.Internal
			                 	: Visibilities.Public;
			return visibility;
		}

		public IInjector<T> Inject(Action<T> injectExpression)
		{
			Ensure.IsNotNull(() => injectExpression);
			EnsureIsValidInjectExpression(injectExpression.Method);
			injectExpression(_target);
			return this;
		}

		internal virtual void EnsureIsValidInjectExpression(MethodInfo injectExpression)
		{
			var method = EnsureIsInlineMethodExpression(injectExpression);
			if (!method.Name.StartsWith("set_", StringComparison.Ordinal))
			{
				EnsureIsValidRertunAndParameter(method);
			}
			EnsureIsVisible(method);
		}

		static MethodInfo EnsureIsInlineMethodExpression(MethodInfo injectExpression)
		{
			try
			{
				var instruction = injectExpression
					.GetInstructions()
					.Single(x => x.OpCode.OperandType == OperandType.InlineMethod);
				return (MethodInfo) instruction.Operand;
			}
			catch (InvalidOperationException exception)
			{
				throw new ArgumentException("The expression is not inline method expression.", exception);
			}
		}

		[SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "InternalVisibleTo")]
		void EnsureIsVisible(MethodInfo method)
		{
			if (!IsVisible(method))
			{
				throw new InvalidOperationException(
					string.Format(CultureInfo.InvariantCulture,
					              "The method '{0}' is not visible. "
					              + "For internal method, set the {1}(\"{2}\") to assembly.",
					              method.Name, "InternalVisibleTo", "DynamicProxyGenAssembly2"));
			}
		}

		bool IsVisible(MethodInfo injectProperty)
		{
			return _methodVisibilityChecker.IsSatisfyVisible(injectProperty);
		}

		static void EnsureIsValidRertunAndParameter(MethodInfo injectMethod)
		{
			var parameters = injectMethod.GetParameters();
			if (injectMethod.ReturnType != typeof (void))
			{
				throw new ArgumentException("The inject method's return type should be void.");
			}
			if (parameters.Length != 1)
			{
				throw new ArgumentException("The inject method's parameter should be one.");
			}
		}

		public IInjector<T> Inject<TAnonymous>(Action<T, TAnonymous> injectExpression)
		{
			Ensure.IsNotNull(() => injectExpression);
			EnsureIsValidInjectExpression(injectExpression.Method);
			var value = (TAnonymous) _valueProvider.Create(typeof (TAnonymous));
			injectExpression(_target, value);
			return this;
		}

		public IInjector<T> InjectAllProperties()
		{
			InjectAllPropertiesImp();
			return this;
		}

		void InjectAllPropertiesImp()
		{
			GetProperties().Where(x => x.CanWrite && IsVisible(x.GetSetMethod(true)))
				.ForEach(x =>
				{
					var value = _valueProvider.Create(x.PropertyType);
					x.SetValue(_target, value, null);
				});
		}

		IEnumerable<PropertyInfo> GetProperties()
		{
			return _target.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
		}

		public IInjector<T> InjectMethods(Func<string, bool> methodNameExpression)
		{
			GetMethods().Where(
				x => IsValidInjectMethod(x)
				     && IsVisible(x)
				     && methodNameExpression(x.Name))
				.ForEach(x =>
				{
					var parameterType = x.GetParameters()[0].ParameterType;
					var value = _valueProvider.Create(parameterType);

					x.Invoke(_target, new[] {value});
				});
			return this;
		}

		IEnumerable<MethodInfo> GetMethods()
		{
			return _target.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
				.Where(x => !x.Name.StartsWith("get_", StringComparison.Ordinal) && !x.Name.StartsWith("set_", StringComparison.Ordinal));
		}

		static bool IsValidInjectMethod(MethodInfo method)
		{
			return method.ReturnType == typeof (void)
			       && method.GetParameters().Length == 1;
		}
	}
}